import * as WebSocket from 'ws';
import * as egret from './ByteArray';

export enum PinusWSClientEvent {
  EVENT_IO_ERROR = 'io-error',
  EVENT_CLOSE = 'close',
  EVENT_KICK = 'onKick',
  EVENT_HEART_BEAT_TIMEOUT = 'heartbeat timeout'
}

export class PinusWSClient {
  public static DEBUG: boolean = false;

  private JS_WS_CLIENT_TYPE = 'js-websocket';
  private JS_WS_CLIENT_VERSION = '0.0.5';

  private RES_OK: number = 200;
  private RES_FAIL: number = 500;
  private RES_OLD_CLIENT: number = 501;

  private socket: WebSocket = null;
  private callbacks: any = {};
  private handlers: any = {};
  // Map from request id to route
  private routeMap: any = {};

  private heartbeatInterval: number = 0;
  private heartbeatTimeout: number = 0;
  private nextHeartbeatTimeout: number = 0;
  private gapThreshold: number = 100;
  private heartbeatId: any = null;
  private heartbeatTimeoutId: any = null;

  private handshakeCallback: any = null;
  private handshakeBuffer: any;
  private initCallback: any = null;

  private _callbacks: any = {};

  private reqId: number = 0;

  private _package: IPackage;
  private _message: IMessage;

  constructor () {
    this.socket = null;
    this.callbacks = {};
    this.handlers = {};
    // Map from request id to route
    this.routeMap = {};
    this._message = new Message(this.routeMap);
    this._package = new Package();

    this.heartbeatInterval = 0;
    this.heartbeatTimeout = 0;
    this.nextHeartbeatTimeout = 0;
    this.gapThreshold = 100;
    this.heartbeatId = null;
    this.heartbeatTimeoutId = null;

    this.handshakeCallback = null;

    this.handshakeBuffer = {
      sys: {
        type: this.JS_WS_CLIENT_TYPE,
        version: this.JS_WS_CLIENT_VERSION
      },
      user: {}
    };

    this.initCallback = null;

    this.reqId = 0;

    this.handlers[Package.TYPE_HANDSHAKE] = this.handshake;
    this.handlers[Package.TYPE_HEARTBEAT] = this.heartbeat;
    this.handlers[Package.TYPE_DATA] = this.onData;
    this.handlers[Package.TYPE_KICK] = this.onKick;
  }

  public init (params: any, cb: any) {
    console.log('init', params);
    this.initCallback = cb;
    let host = params.host;
    let port = params.port;
    this.handshakeBuffer.user = params.user;
    this.handshakeCallback = params.handshakeCallback;
    this.initWebSocket(host, port, cb);
  }

  public on (event: string, fn: (msg: any) => void) {
    (this._callbacks[event] = this._callbacks[event] || []).push(fn);
  }
  public request (route: string, msg: any, cb: any) {
    if (arguments.length === 2 && typeof msg === 'function') {
      cb = msg;
      msg = {};
    } else {
      msg = msg || {};
    }
    route = route || msg.route;
    if (!route) {
      return;
    }

    this.reqId++;
    if (this.reqId > 10000) {
      this.reqId = 1;
    }
    let reqId = this.reqId;

    if (PinusWSClient.DEBUG) {
      console.log(`REQUEST:route:${route} , reqId:${reqId}, msg:${msg}`);
    }

    this.sendMessage(reqId, route, msg);

    this.callbacks[reqId] = cb;
    this.routeMap[reqId] = route;
  }

  public notify (route: string, msg: any) {
    this.sendMessage(0, route, msg);
  }

  public off (event?: string, fn?: any) {
    this.removeAllListeners(event, fn);
  }
  public removeAllListeners (event?: string, fn?: any) {
    // all
    if (0 === arguments.length) {
      this._callbacks = {};
      return;
    }

    // specific event
    let callbacks = this._callbacks[event];
    if (!callbacks) {
      return;
    }

    // remove all handlers
    if (event && !fn) {
      delete this._callbacks[event];
      return;
    }

    // remove specific handler
    let i = this.index(callbacks, (fn)._off || fn);
    if (~i) {
      callbacks.splice(i, 1);
    }
    return;
  }
  public disconnect () {
    this._disconnect();
  }

  private onMessage (event: {
    data: string | Buffer | ArrayBuffer | Buffer[];
    type: string;
    target: WebSocket;
  }) {
    this.processPackage(
      this._package.decode(new egret.ByteArray(event.data as ArrayBuffer))
    );
  }
  private sendMessage (reqId: number, route: string, msg: any) {
    let byte: egret.ByteArray;

    byte = this._message.encode(reqId, route, msg);
    byte = this._package.encode(Package.TYPE_DATA, byte);

    this.send(byte);
  }

  private onConnect () {
    console.log('pinus robot connect success');
    this.send(
      this._package.encode(
        Package.TYPE_HANDSHAKE,
        Protocol.strencode(JSON.stringify(this.handshakeBuffer))
      )
    );
  }

  private onClose (e: any) {
    console.error('pinus robot connect close');
  }

  private onIOError (e: any) {
    console.error('socket error: ', e);
  }

  private onKick (event: string) {
    console.error('pinus robot onKick', event);
  }

  private onData (data: any) {
    // probuff decode
    let msg = this._message.decode(data);

    if (msg.id > 0) {
      msg.route = this.routeMap[msg.id];
      delete this.routeMap[msg.id];
      if (!msg.route) {
        return;
      }
    }

    // msg.body = this.deCompose(msg);

    this.processMessage(msg);
  }

  private processMessage (msg: any) {
    if (!msg.id) {
      // server push message
      this.emit(msg.route, msg.body);
      return;
    }

    // if have a id then find the callback function with the request
    let cb = this.callbacks[msg.id];

    delete this.callbacks[msg.id];
    if (typeof cb !== 'function') {
      return;
    }

    cb(msg.body);
    return;
  }

  private heartbeat (data: any) {
    if (!this.heartbeatInterval) {
      // no heartbeat
      return;
    }

    let obj = this._package.encode(Package.TYPE_HEARTBEAT);
    if (this.heartbeatTimeoutId) {
      clearTimeout(this.heartbeatTimeoutId);
      this.heartbeatTimeoutId = null;
    }

    if (this.heartbeatId) {
      // already in a heartbeat interval
      return;
    }

    let self = this;
    self.heartbeatId = setTimeout(() => {
      self.heartbeatId = null;
      self.send(obj);

      self.nextHeartbeatTimeout = Date.now() + self.heartbeatTimeout;
      self.heartbeatTimeoutId = setTimeout(
        self.heartbeatTimeoutCb.bind(self, data),
        self.heartbeatTimeout
      );
    },                            self.heartbeatInterval);
  }

  private heartbeatTimeoutCb (data: any) {
    let gap = this.nextHeartbeatTimeout - Date.now();
    if (gap > this.gapThreshold) {
      this.heartbeatTimeoutId = setTimeout(
        this.heartbeatTimeoutCb.bind(this, data),
        gap
      );
    } else {
      console.error('server heartbeat timeout', data);
      this.emit(PinusWSClientEvent.EVENT_HEART_BEAT_TIMEOUT, data);
      this._disconnect();
    }
  }

  private index (arr: any, obj: any) {
    if ([].indexOf) {
      return arr.indexOf(obj);
    }

    for (let i = 0; i < arr.length; ++i) {
      if (arr[i] === obj) {
        return i;
      }
    }
    return -1;
  }

  private initWebSocket (host: string, port: number, cb: any): void {
    console.log('connect to:', host, port);

    let url = 'ws://' + host;
    if (port) {
      url += ':' + port;
    }
    let socket = new WebSocket(url);
    socket.binaryType = 'arraybuffer';
    socket.onopen = (event) => {
      this.onConnect();
    };
    socket.onmessage = (event) => {
      this.onMessage(event);
    };
    socket.onerror = (event) => {
      this.onIOError(event);
    };
    socket.onclose = (event) => {
      this.onClose(event);
    };
    this.socket = socket;
  }

  private _disconnect (): void {
    console.warn('pinus robot client disconnect ...');

    if (this.socket) this.socket.close();
    this.socket = null;
    if (this.heartbeatId) {
      clearTimeout(this.heartbeatId);
      this.heartbeatId = null;
    }

    if (this.heartbeatTimeoutId) {
      clearTimeout(this.heartbeatTimeoutId);
      this.heartbeatTimeoutId = null;
    }
  }
  private processPackage (msg: any): void {
    this.handlers[msg.type].apply(this, [msg.body]);
  }
  private handshake (resData: any) {
    let data = JSON.parse(Protocol.strdecode(resData));
    if (data.code === this.RES_OLD_CLIENT) {
      return;
    }

    if (data.code !== this.RES_OK) {
      return;
    }

    this.handshakeInit(data);

    let obj = this._package.encode(Package.TYPE_HANDSHAKE_ACK);
    this.send(obj);
    if (this.initCallback) {
      this.initCallback(data);
      this.initCallback = null;
    }
  }
  private handshakeInit (data: any): void {
    if (data.sys) {
      Routedic.init(data.sys.dict);
      Protobuf.init(data.sys.protos);
    }
    if (data.sys && data.sys.heartbeat) {
      this.heartbeatInterval = data.sys.heartbeat * 1000; // heartbeat interval
      this.heartbeatTimeout = this.heartbeatInterval * 2; // max heartbeat timeout
    } else {
      this.heartbeatInterval = 0;
      this.heartbeatTimeout = 0;
    }

    if (typeof this.handshakeCallback === 'function') {
      this.handshakeCallback(data.user);
    }
  }
  private send (byte: egret.ByteArray) {
    if (this.socket) {
      this.socket.send(byte.buffer);
    }
  }
  //  private deCompose(msg){
  //    return JSON.parse(Protocol.strdecode(msg.body));
  // }
  private emit (event: string, ...args: any[]) {
    let params = [].slice.call(arguments, 1);
    let callbacks = this._callbacks[event];

    if (callbacks) {
      callbacks = callbacks.slice(0);
      for (let i = 0, len = callbacks.length; i < len; ++i) {
        callbacks[i].apply(this, params);
      }
    }

    return this;
  }
}

class Package implements IPackage {
  public static TYPE_HANDSHAKE: number = 1;
  public static TYPE_HANDSHAKE_ACK: number = 2;
  public static TYPE_HEARTBEAT: number = 3;
  public static TYPE_DATA: number = 4;
  public static TYPE_KICK: number = 5;

  public encode (type: number, body?: egret.ByteArray) {
    let length: number = body ? body.length : 0;

    let buffer: egret.ByteArray = new egret.ByteArray();
    buffer.writeByte(type & 0xff);
    buffer.writeByte((length >> 16) & 0xff);
    buffer.writeByte((length >> 8) & 0xff);
    buffer.writeByte(length & 0xff);

    if (body) buffer.writeBytes(body, 0, body.length);

    return buffer;
  }
  public decode (buffer: egret.ByteArray) {
    let type: number = buffer.readUnsignedByte();
    let len: number =
      ((buffer.readUnsignedByte() << 16) |
        (buffer.readUnsignedByte() << 8) |
        buffer.readUnsignedByte()) >>>
      0;

    let body: egret.ByteArray;

    if (buffer.bytesAvailable >= len) {
      body = new egret.ByteArray();
      if (len) buffer.readBytes(body, 0, len);
    } else {
      console.log('[Package] no enough length for current type:', type);
    }

    return { type: type, body: body, length: len };
  }
}

class Message implements IMessage {
  public static MSG_FLAG_BYTES: number = 1;
  public static MSG_ROUTE_CODE_BYTES: number = 2;
  public static MSG_ID_MAX_BYTES: number = 5;
  public static MSG_ROUTE_LEN_BYTES: number = 1;

  public static MSG_ROUTE_CODE_MAX: number = 0xffff;

  public static MSG_COMPRESS_ROUTE_MASK: number = 0x1;
  public static MSG_TYPE_MASK: number = 0x7;

  public static TYPE_REQUEST: number = 0;
  public static TYPE_NOTIFY: number = 1;
  public static TYPE_RESPONSE: number = 2;
  public static TYPE_PUSH: number = 3;

  constructor (private routeMap: any) {}

  public encode (id: number, route: string, msg: any) {
    let buffer: egret.ByteArray = new egret.ByteArray();

    let type: number = id ? Message.TYPE_REQUEST : Message.TYPE_NOTIFY;

    let byte: egret.ByteArray =
      Protobuf.encode(route, msg) || Protocol.strencode(JSON.stringify(msg));

    let rot: any = Routedic.getID(route) || route;

    buffer.writeByte((type << 1) | (typeof rot === 'string' ? 0 : 1));

    if (id) {
      // 7.x
      do {
        let tmp: number = id % 128;
        let next: number = Math.floor(id / 128);

        if (next !== 0) {
          tmp = tmp + 128;
        }

        buffer.writeByte(tmp);

        id = next;
      } while (id !== 0);

      // 5.x
      //                var len:Array = [];
      //                len.push(id & 0x7f);
      //                id >>= 7;
      //                while(id > 0)
      //                {
      //                    len.push(id & 0x7f | 0x80);
      //                    id >>= 7;
      //                }
      //
      //                for (var i:int = len.length - 1; i >= 0; i--)
      //                {
      //                    buffer.writeByte(len[i]);
      //                }
    }

    if (rot) {
      if (typeof rot === 'string') {
        buffer.writeByte(rot.length & 0xff);
        buffer.writeUTFBytes(rot);
      } else {
        buffer.writeByte((rot >> 8) & 0xff);
        buffer.writeByte(rot & 0xff);
      }
    }

    if (byte) {
      buffer.writeBytes(byte);
    }

    return buffer;
  }

  public decode (buffer: egret.ByteArray): any {
    // parse flag
    let flag: number = buffer.readUnsignedByte();
    let compressRoute: number = flag & Message.MSG_COMPRESS_ROUTE_MASK;
    let type: number = (flag >> 1) & Message.MSG_TYPE_MASK;
    let route: any;

    // parse id
    let id: number = 0;
    if (type === Message.TYPE_REQUEST || type === Message.TYPE_RESPONSE) {
      // 7.x
      let i: number = 0;
      let m: number;
      do {
        m = buffer.readUnsignedByte();
        id = id + (m & 0x7f) * Math.pow(2, 7 * i);
        i++;
      } while (m >= 128);

      // 5.x
      //                var byte:int = buffer.readUnsignedByte();
      //                id = byte & 0x7f;
      //                while(byte & 0x80)
      //                {
      //                    id <<= 7;
      //                    byte = buffer.readUnsignedByte();
      //                    id |= byte & 0x7f;
      //                }
    }

    // parse route
    if (
      type === Message.TYPE_REQUEST ||
      type === Message.TYPE_NOTIFY ||
      type === Message.TYPE_PUSH
    ) {
      if (compressRoute) {
        route = buffer.readUnsignedShort();
      } else {
        let routeLen: number = buffer.readUnsignedByte();
        route = routeLen ? buffer.readUTFBytes(routeLen) : '';
      }
    } else if (type === Message.TYPE_RESPONSE) {
      route = this.routeMap[id];
    }

    if (!id && !(typeof route === 'string')) {
      route = Routedic.getName(route);
    }

    let body: any =
      Protobuf.decode(route, buffer) || JSON.parse(Protocol.strdecode(buffer));

    return { id: id, type: type, route: route, body: body };
  }
}
class Protocol {
  public static strencode (str: string): egret.ByteArray {
    let buffer: egret.ByteArray = new egret.ByteArray();
    buffer.length = str.length;
    buffer.writeUTFBytes(str);
    return buffer;
  }

  public static strdecode (byte: egret.ByteArray): string {
    return byte.readUTFBytes(byte.bytesAvailable);
  }
}
class Protobuf {
  public static TYPES: any = {
    uInt32: 0,
    sInt32: 0,
    int32: 0,
    double: 1,
    string: 2,
    message: 2,
    float: 5
  };
  private static _clients: any = {};
  private static _servers: any = {};

  public static init (protos: any) {
    this._clients = (protos && protos.client) || {};
    this._servers = (protos && protos.server) || {};
  }

  public static encode (route: string, msg: any): egret.ByteArray {
    let protos: any = this._clients[route];

    if (!protos) return null;

    return this.encodeProtos(protos, msg);
  }

  public static decode (route: string, buffer: egret.ByteArray): any {
    let protos: any = this._servers[route];

    if (!protos) return null;

    return this.decodeProtos(protos, buffer);
  }
  public static decodeProtos (protos: any, buffer: egret.ByteArray): any {
    let msg: any = {};

    while (buffer.bytesAvailable) {
      let head: any = this.getHead(buffer);
      let name: string = protos.__tags[head.tag];

      switch (protos[name].option) {
        case 'optional':
        case 'required':
          msg[name] = this.decodeProp(protos[name].type, protos, buffer);
          break;
        case 'repeated':
          if (!msg[name]) {
            msg[name] = [];
          }
          this.decodeArray(msg[name], protos[name].type, protos, buffer);
          break;
      }
    }

    return msg;
  }

  public static encodeTag (type: number, tag: number): egret.ByteArray {
    let value: number = this.TYPES[type] !== undefined ? this.TYPES[type] : 2;

    return this.encodeUInt32((tag << 3) | value);
  }
  public static getHead (buffer: egret.ByteArray): any {
    let tag: number = this.decodeUInt32(buffer);

    return { type: tag & 0x7, tag: tag >> 3 };
  }
  public static encodeProp (
    value: any,
    type: string,
    protos: any,
    buffer: egret.ByteArray
  ) {
    switch (type) {
      case 'uInt32':
        buffer.writeBytes(this.encodeUInt32(value));
        break;
      case 'int32':
      case 'sInt32':
        buffer.writeBytes(this.encodeSInt32(value));
        break;
      case 'float':
        // Float32Array
        let floats: egret.ByteArray = new egret.ByteArray();
        floats.endian = egret.Endian.LITTLE_ENDIAN;
        floats.writeFloat(value);
        buffer.writeBytes(floats);
        break;
      case 'double':
        let doubles: egret.ByteArray = new egret.ByteArray();
        doubles.endian = egret.Endian.LITTLE_ENDIAN;
        doubles.writeDouble(value);
        buffer.writeBytes(doubles);
        break;
      case 'string':
        buffer.writeBytes(this.encodeUInt32(value.length));
        buffer.writeUTFBytes(value);
        break;
      default:
        let proto: any =
          protos.__messages[type] || this._clients['message ' + type];
        if (!!proto) {
          let buf: egret.ByteArray = this.encodeProtos(proto, value);
          buffer.writeBytes(this.encodeUInt32(buf.length));
          buffer.writeBytes(buf);
        }
        break;
    }
  }

  public static decodeProp (
    type: string,
    protos: any,
    buffer: egret.ByteArray
  ): any {
    switch (type) {
      case 'uInt32':
        return this.decodeUInt32(buffer);
      case 'int32':
      case 'sInt32':
        return this.decodeSInt32(buffer);
      case 'float':
        let floats: egret.ByteArray = new egret.ByteArray();
        buffer.readBytes(floats, 0, 4);
        floats.endian = egret.Endian.LITTLE_ENDIAN;
        let float: number = buffer.readFloat();
        return floats.readFloat();
      case 'double':
        let doubles: egret.ByteArray = new egret.ByteArray();
        buffer.readBytes(doubles, 0, 8);
        doubles.endian = egret.Endian.LITTLE_ENDIAN;
        return doubles.readDouble();
      case 'string':
        let length: number = this.decodeUInt32(buffer);
        return buffer.readUTFBytes(length);
      default:
        let proto: any =
          protos &&
          (protos.__messages[type] || this._servers['message ' + type]);
        if (proto) {
          let len: number = this.decodeUInt32(buffer);
          let buf: egret.ByteArray;
          if (len) {
            buf = new egret.ByteArray();
            buffer.readBytes(buf, 0, len);
          }

          return len ? Protobuf.decodeProtos(proto, buf) : false;
        }
        break;
    }
  }

  public static isSimpleType (type: string): boolean {
    return (
      type === 'uInt32' ||
      type === 'sInt32' ||
      type === 'int32' ||
      type === 'uInt64' ||
      type === 'sInt64' ||
      type === 'float' ||
      type === 'double'
    );
  }
  public static encodeArray (
    array: any[],
    proto: any,
    protos: any,
    buffer: egret.ByteArray
  ) {
    let isSimpleType = this.isSimpleType;
    if (isSimpleType(proto.type)) {
      buffer.writeBytes(this.encodeTag(proto.type, proto.tag));
      buffer.writeBytes(this.encodeUInt32(array.length));
      let encodeProp = this.encodeProp;
      for (let i: number = 0; i < array.length; i++) {
        encodeProp(array[i], proto.type, protos, buffer);
      }
    } else {
      let encodeTag = this.encodeTag;
      for (let j: number = 0; j < array.length; j++) {
        buffer.writeBytes(encodeTag(proto.type, proto.tag));
        this.encodeProp(array[j], proto.type, protos, buffer);
      }
    }
  }
  public static decodeArray (
    array: any[],
    type: string,
    protos: any,
    buffer: egret.ByteArray
  ) {
    let isSimpleType = this.isSimpleType;
    let decodeProp = this.decodeProp;

    if (isSimpleType(type)) {
      let length: number = this.decodeUInt32(buffer);
      for (let i: number = 0; i < length; i++) {
        array.push(decodeProp(type, protos, buffer));
      }
    } else {
      array.push(decodeProp(type, protos, buffer));
    }
  }

  public static encodeUInt32 (n: number): egret.ByteArray {
    let result: egret.ByteArray = new egret.ByteArray();

    do {
      let tmp: number = n % 128;
      let next: number = Math.floor(n / 128);

      if (next !== 0) {
        tmp = tmp + 128;
      }

      result.writeByte(tmp);
      n = next;
    } while (n !== 0);

    return result;
  }
  public static decodeUInt32 (buffer: egret.ByteArray): number {
    let n: number = 0;

    for (let i: number = 0; i < buffer.length; i++) {
      let m: number = buffer.readUnsignedByte();
      n = n + (m & 0x7f) * Math.pow(2, 7 * i);
      if (m < 128) {
        return n;
      }
    }
    return n;
  }
  public static encodeSInt32 (n: number): egret.ByteArray {
    n = n < 0 ? Math.abs(n) * 2 - 1 : n * 2;

    return this.encodeUInt32(n);
  }
  public static decodeSInt32 (buffer: egret.ByteArray): number {
    let n: number = this.decodeUInt32(buffer);

    let flag: number = n % 2 === 1 ? -1 : 1;

    n = (((n % 2) + n) / 2) * flag;

    return n;
  }

  private static encodeProtos (protos: any, msg: any): egret.ByteArray {
    let buffer: egret.ByteArray = new egret.ByteArray();

    for (let name in msg) {
      if (protos[name]) {
        let proto: any = protos[name];

        switch (proto.option) {
          case 'optional':
          case 'required':
            buffer.writeBytes(this.encodeTag(proto.type, proto.tag));
            this.encodeProp(msg[name], proto.type, protos, buffer);
            break;
          case 'repeated':
            if (!!msg[name] && msg[name].length > 0) {
              this.encodeArray(msg[name], proto, protos, buffer);
            }
            break;
        }
      }
    }

    return buffer;
  }
}
class Routedic {
  private static _ids: any = {};
  private static _names: any = {};

  public static init (dict: any) {
    this._names = dict || {};
    let _names = this._names;
    let _ids = this._ids;
    for (let name in _names) {
      _ids[_names[name]] = name;
    }
  }

  public static getID (name: string) {
    return this._names[name];
  }
  public static getName (id: number) {
    return this._ids[id];
  }
}

interface IMessage {
  /**
   * encode
   * @param id
   * @param route
   * @param msg
   * @return ByteArray
   */
  encode (id: number, route: string, msg: any): egret.ByteArray;

  /**
   * decode
   * @param buffer
   * @return Object
   */
  decode (buffer: egret.ByteArray): any;
}
interface IPackage {
  encode (type: number, body?: egret.ByteArray): egret.ByteArray;

  decode (buffer: egret.ByteArray): any;
}
